/*
This file is part of [ahttp] library. 

Author: Artem Kustikov (kustikoff[at]tut.by)


This code is provided 'as-is', without any express or implied
warranty. In no event will the authors be held liable for any
damages arising from the use of this code.

Permission is granted to anyone to use this code for any
purpose, including commercial applications, and to alter it and
redistribute it freely, subject to the following restrictions:

1. The origin of this code must not be misrepresented; you must
not claim that you wrote the original code. If you use this
code in a product, an acknowledgment in the product documentation
would be appreciated but is not required.

2. Altered source versions must be plainly marked as such, and
must not be misrepresented as being the original code.

3. This notice may not be removed or altered from any source
distribution.
*/

#include "aconnect/boost_format_safe.hpp"

#include <assert.h>
#include <boost/lexical_cast.hpp>

#include "aconnect/server.hpp"
#include "aconnect/util.hpp"
#include "aconnect/util.time.hpp"
#include "aconnect/compression/zlib_compressor.hpp"

#include "ahttp/http_context.hpp"

namespace ahttp
{

	//////////////////////////////////////////////////////////////////////////
	//
	//		HttpResponse
	//

	void HttpResponse::fillCommonResponseHeaders () 
	{
		if (!Header.hasHeader (strings::HeaderServer) && !_serverName.empty() )
			Header.Headers [strings::HeaderServer] = _serverName;
		
		if (!Header.hasHeader (strings::HeaderDate))
			Header.Headers [strings::HeaderDate] = formatDate_RFC1123 (aconnect::util::getDateTimeUtc ());

		if ( Header.Status == HttpStatus::OK && !Header.hasHeader (strings::HeaderContentType) )
			Header.setContentType (strings::ContentTypeTextHtml);

		if (!_context->GlobalSettings->IsKeepAliveEnabled)
			Header.setConnectionClose();
	}

	void HttpResponse::sendHeaders () throw (std::runtime_error)
	{
		if (_headersSent)
			throw std::runtime_error ("HTTP headers already sent");

		_context->runModules(ModuleCallbackOnResponsePreSendHeaders);
		
		applyTransferEncoding();
		
		applyContentEncoding ();

		fillCommonResponseHeaders();
				
		// Headers send as plain text - write without modification
		aconnect::util::writeToSocket (_clientInfo->sock, 
			Header.getContent(), true);
		
		_headersSent = true;

		_context->runModules(ModuleCallbackOnResponsePreSendContent);
	}

	void HttpResponse::sendHeadersContent (string_constptr status, string_constptr headers) throw (std::runtime_error)
	{
		assert ( NULL != status && _httpMethod != ahttp::HttpMethod::Unknown && "HTTP method is not loaded");
		if (_headersSent)
			throw std::runtime_error ("HTTP headers already sent");

		Header.load (status, headers);
		
		sendHeaders ();
	}

	void HttpResponse::setHeader (string_constref headerName, string_constref headerValue)
	{
		if (_headersSent)
			throw std::runtime_error ("HTTP headers already sent");
		
		Header.setHeader (headerName, headerValue);
	}

	void HttpResponse::applyTransferEncoding () 
	{
		if (_finished)
			throw std::runtime_error ("Response already sent");

		if ( !Header.hasHeader (strings::HeaderContentLength) 
				&& !Header.hasHeader (strings::HeaderTransferEncoding)) 
		{
			Stream.setChunkedMode ();
			Header.Headers[strings::HeaderTransferEncoding] = strings::TransferEncodingChunked;
		}
	}

	void HttpResponse::applyContentEncoding () 
	{
		// check encoding setup by handler/module 
		if (_preferedContentEncoding != ContentEncoding::Identity 
				&& !Header.hasHeader (strings::HeaderContentEncoding))
		{
			Header.setContentEncoding (_preferedContentEncoding);
			Stream.setContentEncoding (_preferedContentEncoding);
		}
		else if (_preferedContentEncoding == ContentEncoding::Identity && !_headersSent)
		{
			Header.setContentEncoding (ContentEncoding::Identity);
			Stream.setContentEncoding (ContentEncoding::Identity);
		}
	}

	void HttpResponse::writeCompleteHtmlResponse (string_constref response) throw (std::runtime_error) 
	{
		Header.setContentType (strings::ContentTypeTextHtml);

		writeCompleteResponse (response);
	}

	void HttpResponse::writeCompleteResponse (string_constref response) throw (std::runtime_error)
	{
		assert ( !_finished && "Response already sent" );
		assert ( !_headersSent && "Headers already sent" );

		if (_headersSent)
			throw std::runtime_error ("HTTP headers already sent");
		if (_finished)
			throw std::runtime_error ("Response already sent");

		applyContentEncoding ();

		Stream.clear();
		
		Stream.writeCompleteResponse (response, 
			std::bind1st (std::mem_fun(&HttpResponse::sendHeadersWithContentLength), this) );
		
		_finished = true;
	}

	void HttpResponse::sendHeadersWithContentLength (size_t length) throw (std::runtime_error)
	{
		Header.setContentLength (length);

		sendHeaders();
	}

	void HttpResponse::write (string_constptr content, const size_t contentSize) 
	{
		if (_finished)
			throw std::runtime_error ("Response already sent");

		if (!_headersSent && Stream.willBeFlushed ( contentSize )) {
			
			if (_preferedContentEncoding != ContentEncoding::Identity
					&& !Header.hasHeader (strings::HeaderContentEncoding)) { // Content-Encoding setup externally
				
				Header.removeHeader (strings::HeaderContentLength); // reset to chuncked mode
			}
			
			sendHeaders();

		} else {
			applyContentEncoding ();
		}

		Stream.write (content, contentSize);
	}

	void HttpResponse::writeString (string_constref content) 
	{
		if (_finished)
			throw std::runtime_error ("Response already sent");

		write (content.c_str(), content.size() );
	}

	void HttpResponse::flush () throw (aconnect::socket_error) 
	{
		if (_finished)
			return;

		if (!_headersSent)
			sendHeaders();
		
		Stream.flush();
	}

	void HttpResponse::end () throw (aconnect::socket_error) 
	{
		if (_finished)
			return;
		
		// setup correct content length
		if (!_headersSent) 
		{
			Stream.finishContentCompession ();
			Header.setContentLength ( Stream.getBufferContentSize() );
			
			sendHeaders();
		}
		
		Stream.end();

		_context->runModules (ModuleCallbackOnResponseEnd); 
		
		_finished = true;
	}

	//////////////////////////////////////////////////////////////////////////
	//
	//		Static helpers
	//
	string HttpResponse::getErrorResponse (int status, 
		const HttpServerSettings &settings,
		string_constptr messageFormat, ...)
	{
		string statusDesc = strings::httpStatusDesc (status);
		string description;

		if (messageFormat) {
			FORMAT_VA_MESSAGE (messageFormat, message);
			description.swap (message);
		
		} else {
			description = settings.getMessage("ErrorUndefined");
		}

		string_constref msgFormat = settings.getMessage("MessageFormat");
		
		size_t buffSize = aconnect::util::alignInt8 (msgFormat.size() + statusDesc.size()*2 + description.size() + 8);
		boost::scoped_array<char_type> buff (new char_type[ buffSize ]);

		int formatted = snprintf (buff.get(), 
			buffSize, 
			settings.getMessage("MessageFormat").c_str(), 
			statusDesc.c_str(),
			statusDesc.c_str(),
			description.c_str());

		assert (formatted > 0);
		
		return string(buff.get(), formatted);
	}

	//
	//
	//////////////////////////////////////////////////////////////////////////


	//////////////////////////////////////////////////////////////////////////
	//
	//		HttpResponseStream
	//
	
	void HttpResponseStream::write (string_constptr content, const size_t contentSize)
	{	
		if (contentSize == 0)
			return;

		string_constptr processedContent = content;
		size_t processedContentSize = contentSize;
		string processedContentStorage;

		if (_contentEncoding != ContentEncoding::Identity) {
			processedContentSize = applyContentEncoding(content, contentSize, processedContentStorage, false);
			processedContent = processedContentStorage.c_str();
		}

		const size_t newBuffSize = _buffer.size() + processedContentSize;

		if (newBuffSize >= _maxBuffSize) {

			if (_buffer.size() > 0)
				flush();

			processFlush(processedContent, processedContentSize);

		} else {
			_buffer.append (processedContent, processedContentSize);
		}
	};

	void HttpResponseStream::writeCompleteResponse (string_constref content, 
		boost::function1<void, size_t> sendHeadersCallback) throw (aconnect::socket_error)
	{
		assert (!_chunked && "writeCompleteResponse cannot be called in 'chunked' mode");

		if (!_writeResponseBody)
			return;

		string_constptr processedContent = content.c_str();
		size_t processedContentSize = content.size();
		string processedContentStorage;

		if (_contentEncoding != ContentEncoding::Identity) {
			processedContentSize = applyContentEncoding(processedContent, processedContentSize, processedContentStorage, true);
			processedContent = processedContentStorage.c_str();
		}

		// inform about processed content length
		sendHeadersCallback (processedContentSize);

		aconnect::util::writeToSocket (_socket, processedContent, processedContentSize, true);
	}

	void HttpResponseStream::setContentEncoding (ContentEncoding::ContentEncodingType encoding) 
	{
#if defined (ACONNECT_USE_ZLIB)
		using aconnect::compression::zlib::GzipCompressor;
		using aconnect::compression::zlib::GzipSettings;

		_contentEncoding = encoding;

		if (encoding == ContentEncoding::Deflate)
			_compressor.reset (new GzipCompressor (GzipSettings()));
		else if (encoding == ContentEncoding::Gzip)
			_compressor.reset (new GzipCompressor (GzipSettings(false)));
#endif // #if defined (ACONNECT_USE_ZLIB)
	}

	size_t HttpResponseStream::applyContentEncoding (string_constptr content, const size_t contentSize, aconnect::string& storage, bool lastPart)
	{
		assert (_contentEncoding != ContentEncoding::Identity);
		assert (_contentEncoding == ContentEncoding::Deflate 
			|| _contentEncoding == ContentEncoding::Gzip); // only gzip supported
		
		_compressor->compress (content, contentSize, storage, lastPart);

		return storage.size();
	}

	void HttpResponseStream::flush () throw (aconnect::socket_error)
	{	
		using namespace aconnect;

		if (_buffer.empty())
			return;
		
		if (!_writeResponseBody)
			return;

		processFlush(_buffer.c_str(), _buffer.size());

		_buffer.clear();
	};

	void HttpResponseStream::processFlush (string_constptr content, const size_t contentSize) throw (aconnect::socket_error)
	{
		if (_chunked) {

			size_t curPos = 0, chunkSize = contentSize;
			
			const int chunkLenBufferSize = 8;
			char_type chunkLenBuffer[chunkLenBufferSize] = {0};
			int formatted = 0;

			if (chunkSize > _maxChunkSize)
				chunkSize = _maxChunkSize;
			do 
			{
				// write chunk size
				formatted = snprintf (chunkLenBuffer, chunkLenBufferSize, strings::ChunkHeaderFormat, chunkSize);
				assert (formatted > 0 && "Error formatting chunk size");
				util::writeToSocket (_socket, chunkLenBuffer, formatted);
				
				// write data
				util::writeToSocket (_socket, content + curPos, 
					(int) chunkSize);
				
				// write chunk end mark
				util::writeToSocket (_socket, strings::ChunkEndMark);

				curPos += chunkSize;
				chunkSize = util::min2 (_maxChunkSize, contentSize - curPos);

			} while (curPos < contentSize);
			
		} else {
			util::writeToSocket (_socket, content, contentSize);
		}
	}

	void HttpResponseStream::finishContentCompession()
	{
		if (_contentEncoding == ContentEncoding::Identity) 
			return;

		assert (_contentEncoding == ContentEncoding::Deflate || _contentEncoding == ContentEncoding::Gzip); // only gzip supported

		// write directly to buffer
		_compressor->compress (NULL, 0, _buffer, true);
	}

	void HttpResponseStream::end () throw (aconnect::socket_error, aconnect::application_error)
	{	

		flush();

		if (_contentEncoding != ContentEncoding::Identity) 
			_compressor->complete();
		
		if (_chunked && _writeResponseBody) {
			// write last chunk
			aconnect::util::writeToSocket (_socket, string (strings::LastChunkFormat) );
		}
	};

	void HttpResponseStream::setupFileInfo(aconnect::string_constptr fileName, std::time_t fileModificationTime) 
	{
		if (_contentEncoding == ContentEncoding::Gzip)
		{
			aconnect::compression::zlib::GzipCompressor* gzipper = dynamic_cast<aconnect::compression::zlib::GzipCompressor*> (_compressor.get());
			if (gzipper)
				gzipper->setupFileInfo (fileName, fileModificationTime);
		}
	}
}
//
//
//////////////////////////////////////////////////////////////////////////

